/* Copyright (C) 2014  Jean-Sebastien Castonguay
 *
 * This file is part of TechnoBoard.
 *
 * TechnoBoard is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * TechnoBoard is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with TechnoBoard.  If not, see <http://www.gnu.org/licenses/>. */

#include "usb_user.h"
#include "technoboard.h"

#include <system.h>
#include <system_config.h>

#include <app_device_cdc_basic.h>
#include <app_led_usb_status.h>

#include <usb/usb.h>
#include <usb/usb_device.h>
#include <usb/usb_device_cdc.h>

#include <limits.h>
#include <string.h>
#include <stdio.h>

#define STRING_SIZE  16

#define TX_BUFFER_SIZE  UCHAR_MAX  // Limit of Microchip CDC implementation
typedef struct {
    uint8_t data[TX_BUFFER_SIZE];
    uint8_t sizeInBuffer;
} TxBuffer;

static TxBuffer txDoubleBuffer[2];
static TxBuffer * readTxBuffer = &txDoubleBuffer[0];
static TxBuffer * writeTxBuffer = &txDoubleBuffer[1];
static inline void SwapTxBuffer() {
    TxBuffer * tmp = readTxBuffer;
    readTxBuffer = writeTxBuffer;
    writeTxBuffer = tmp;
}


#define RX_BUFFER_SIZE  CDC_DATA_OUT_EP_SIZE  // See Microchip CDC implementation
typedef struct {
    uint8_t data[RX_BUFFER_SIZE];
    uint8_t readIndex;
    uint8_t writeIndex;
} RxBuffer;

static RxBuffer rxBuffer;
static inline unsigned int SizeInRxBuffer() {
    return (rxBuffer.writeIndex - rxBuffer.readIndex);
}


void USBUser_Initialise() {
    
    readTxBuffer->sizeInBuffer = 0;
    writeTxBuffer->sizeInBuffer = 0;

    rxBuffer.readIndex = 0;
    rxBuffer.writeIndex = 0;

    USBDeviceInit();
    USBDeviceAttach();
}


unsigned int USB_WriteData( uint8_t * data, unsigned int size) {

    unsigned int sizeWritten;

    sizeWritten = MIN( size, (unsigned int)(TX_BUFFER_SIZE - writeTxBuffer->sizeInBuffer));

    memcpy( &(writeTxBuffer->data[writeTxBuffer->sizeInBuffer]), data, sizeWritten);
    writeTxBuffer->sizeInBuffer += sizeWritten;

    return sizeWritten;
}


unsigned int USB_PrintString( char * string) {

    return USB_WriteData( (uint8_t *)string, strlen( string));
}

unsigned int USB_PrintInt( int value) {
    
    char string[STRING_SIZE];

    snprintf( string, STRING_SIZE, "%d", value);
    return USB_PrintString( string);
}


static void ManageTx() {

    if (USBUSARTIsTxTrfReady() == true) {
        if (writeTxBuffer->sizeInBuffer > 0) {
            INTERRUPT_PROTECT( SwapTxBuffer(); writeTxBuffer->sizeInBuffer = 0;);
            putUSBUSART( readTxBuffer->data, readTxBuffer->sizeInBuffer);
        }
    }
}


unsigned int USB_ReadData( uint8_t * data, unsigned int size) {

    unsigned int sizeRead = MIN( size, SizeInRxBuffer());

    if (sizeRead > 0) {
        memcpy( data, &(rxBuffer.data[rxBuffer.readIndex]), sizeRead);
        rxBuffer.readIndex += sizeRead;
    }
    
    return sizeRead;
}


bool USB_IsDataReceived() {
    return (SizeInRxBuffer() > 0);
}


char USB_GetChar() {
    
    char byte;

    if (SizeInRxBuffer() > 0) {
        byte = (char)rxBuffer.data[rxBuffer.readIndex];
        rxBuffer.readIndex++;
    } else {
        byte = '\0';
    }

    return byte;
}


static void ManageRx() {

    if (SizeInRxBuffer() == 0) {
        unsigned int sizeRead = getsUSBUSART(rxBuffer.data, RX_BUFFER_SIZE);
        INTERRUPT_PROTECT( rxBuffer.writeIndex = sizeRead; rxBuffer.readIndex = 0);
    }
}


void USBUser_Process() {
    
    #if defined(USB_POLLING)
        // Interrupt or polling method.  If using polling, must call
        // this function periodically.  This function will take care
        // of processing and responding to SETUP transactions
        // (such as during the enumeration process when you first
        // plug in).  USB hosts require that USB devices should accept
        // and process SETUP packets in a timely fashion.  Therefore,
        // when using polling, this function should be called
        // regularly (such as once every 1.8ms or faster** [see
        // inline code comments in usb_device.c for explanation when
        // "or faster" applies])  In most cases, the USBDeviceTasks()
        // function does not take very long to execute (ex: <100
        // instruction cycles) before it returns.
        USBDeviceTasks();
    #endif


    /* If the USB device isn't configured yet, we can't really do anything
     * else since we don't have a host to talk to.  So jump back to the
     * top of the while loop. */
    if( USBGetDeviceState() < CONFIGURED_STATE )
    {
        /* Jump back to the top of the while loop. */
        return;
    }

    /* If we are currently suspended, then we need to see if we need to
     * issue a remote wakeup.  In either case, we shouldn't process any
     * keyboard commands since we aren't currently communicating to the host
     * thus just continue back to the start of the while loop. */
    if( USBIsDeviceSuspended()== true )
    {
        /* Jump back to the top of the while loop. */
        return;
    }

#if 0
    //Application specific tasks
    APP_DeviceCDCBasicDemoTasks();
#else
    ManageTx();
    ManageRx();
    CDCTxService();
#endif
}


bool USER_USB_CALLBACK_EVENT_HANDLER(USB_EVENT event, void *pdata, uint16_t size)
{
    (void)pdata;
    (void)size;

    switch( (int) event )
    {
        case EVENT_TRANSFER:
            break;

        case EVENT_SOF:
            /* We are using the SOF as a timer to time the LED indicator.  Call
             * the LED update function here. */
            //APP_LEDUpdateUSBStatus();
            break;

        case EVENT_SUSPEND:
            /* Update the LED status for the suspend event. */
            //APP_LEDUpdateUSBStatus();
            break;

        case EVENT_RESUME:
            /* Update the LED status for the resume event. */
            //APP_LEDUpdateUSBStatus();
            break;

        case EVENT_CONFIGURED:
            /* When the device is configured, we can (re)initialize the
             * demo code. */
            APP_DeviceCDCBasicDemoInitialize();
            break;

        case EVENT_SET_DESCRIPTOR:
            break;

        case EVENT_EP0_REQUEST:
            /* We have received a non-standard USB request.  The HID driver
             * needs to check to see if the request was for it. */
            USBCheckCDCRequest();
            break;

        case EVENT_BUS_ERROR:
            break;

        case EVENT_TRANSFER_TERMINATED:
            break;

        default:
            break;
    }
    return true;
}
